/* * Copyright (C) 2009 - 2013 Niall 'Rivernile' Scott * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors or contributors be held liable for * any damages arising from the use of this software. * * The aforementioned copyright holder(s) hereby grant you a * non-transferrable right to use this software for any purpose (including * commercial applications), and to modify it and redistribute it, subject to * the following conditions: * * 1. This notice may not be removed or altered from any file it appears in. * * 2. Any modifications made to this software, except those defined in * clause 3 of this agreement, must be released under this license, and * the source code of any modifications must be made available on a * publically accessible (and locateable) website, or sent to the * original author of this software. * * 3. Software modifications that do not alter the functionality of the * software but are simply adaptations to a specific environment are * exempt from clause 2. */ package uk.org.rivernile.edinburghbustracker.android.fragments.general; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.text.Html; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.TouchDelegate; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.SimpleExpandableListAdapter; import android.widget.TextView; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import uk.org.rivernile.android.bustracker.parser.livetimes.Bus; import uk.org.rivernile.android.bustracker.parser.livetimes.BusParser; import uk.org.rivernile.android.bustracker.parser.livetimes.BusService; import uk.org.rivernile.android.bustracker.parser.livetimes.BusStop; import uk.org.rivernile.android.bustracker.parser.livetimes.BusTimesLoader; import uk.org.rivernile.android.bustracker.parser.livetimes.BusTimesResult; import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase; import uk.org.rivernile.edinburghbustracker.android.PreferencesActivity; import uk.org.rivernile.edinburghbustracker.android.R; import uk.org.rivernile.edinburghbustracker.android.SettingsDatabase; import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs .DeleteFavouriteDialogFragment; import uk.org.rivernile.edinburghbustracker.android.livetimes.parser .EdinburghBus; import uk.org.rivernile.edinburghbustracker.android.livetimes.parser .EdinburghBusStop; import uk.org.rivernile.edinburghbustracker.android.livetimes.parser .EdinburghParser; /** * This fragment shows live bus times. It is perhaps the most important part of * the application. There are a few things to note; * * - This fragment communicates with the BusTimes loader. It is a singleton * instance which holds the result between rotation changes. * - There is a progress view, bus times view and error view. This simply * enables and disables layouts as required. * - The menu item enabled states change depending on whether bus times are * being displayed or not. * - The bus stop name shown is taken from the favourite stops list or the bus * stop database or finally from the bus tracker service. * * @author Niall Scott */ public class DisplayStopDataFragment extends Fragment implements LoaderManager.LoaderCallbacks<BusTimesResult>, DeleteFavouriteDialogFragment.Callbacks { private static final int EVENT_REFRESH = 1; private static final int EVENT_UPDATE_TIME = 2; private static final String SERVICE_NAME_KEY = "SERVICE_NAME"; private static final String DESTINATION_KEY = "DESTINATION"; private static final String ARRIVAL_TIME_KEY = "ARRIVAL_TIME"; /** This is the stop code argument. */ public static final String ARG_STOPCODE = "stopCode"; /** This is the argument required to force a reload of data. */ public static final String ARG_FORCELOAD = "forceLoad"; private static final String LOADER_ARG_STOPCODES = "stopCodes"; private static final String LOADER_ARG_NUMBER_OF_DEPARTURES = "numberOfDepartures"; private static final String STATE_KEY_AUTOREFRESH = "autoRefresh"; private static final String STATE_KEY_LAST_REFRESH = "lastRefresh"; private static final String STATE_KEY_EXPANDED_ITEMS = "expandedItems"; private static final int AUTO_REFRESH_PERIOD = 60000; private static final int LAST_REFRESH_PERIOD = 10000; private Callbacks callbacks; private BusStopDatabase bsd; private SettingsDatabase sd; private SharedPreferences sp; private ExpandableListView listView; private TextView txtLastRefreshed, txtStopName, txtServices, txtError; private BusTimesExpandableListAdapter listAdapter; private View layoutTopBar; private ProgressBar progressSmall, progressBig; private ImageButton imgbtnFavourite; private int numDepartures = 4; private String stopCode; private String stopName; private String stopLocality; private boolean autoRefresh; private long lastRefresh = 0; private final ArrayList<String> expandedServices = new ArrayList<String>(); private boolean busTimesLoading = false; private int hitboxSize; /** * Create a new instance of this Fragment, specifying the bus stop code. * * @param stopCode The stopCode to load times for. * @return A new instance of this Fragment. */ public static DisplayStopDataFragment newInstance(final String stopCode) { final DisplayStopDataFragment f = new DisplayStopDataFragment(); final Bundle b = new Bundle(); b.putString(ARG_STOPCODE, stopCode); f.setArguments(b); return f; } /** * Create a new instance of this Fragment, specifying the bus stop code and * if a load of the data should be forced. * * @param stopCode The stopCode to load times for. * @param forceLoad true if data is to be refreshed, false if not. * @return A new instance of this Fragment. */ public static DisplayStopDataFragment newInstance(final String stopCode, final boolean forceLoad) { final DisplayStopDataFragment f = new DisplayStopDataFragment(); final Bundle b = new Bundle(); b.putString(ARG_STOPCODE, stopCode); b.putBoolean(ARG_FORCELOAD, forceLoad); f.setArguments(b); return f; } /** * {@inheritDoc} */ @Override public void onAttach(final Activity activity) { super.onAttach(activity); try { callbacks = (Callbacks) activity; } catch (ClassCastException e) { throw new IllegalStateException(activity.getClass().getName() + " does not implement " + Callbacks.class.getName()); } } /** * {@inheritDoc} */ @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get the various resources we need. final Context context = getActivity().getApplicationContext(); bsd = BusStopDatabase.getInstance(context); sd = SettingsDatabase.getInstance(context); sp = context.getSharedPreferences(PreferencesActivity.PREF_FILE, 0); hitboxSize = getResources() .getDimensionPixelOffset(R.dimen.star_hitbox_size); // Get the stop code from the arguments bundle. stopCode = getArguments().getString(ARG_STOPCODE); // Get preferences. try { numDepartures = Integer.parseInt( sp.getString(PreferencesActivity .PREF_NUMBER_OF_SHOWN_DEPARTURES_PER_SERVICE, "4")); } catch(NumberFormatException e) { numDepartures = 4; } if(savedInstanceState != null) { lastRefresh = savedInstanceState.getLong(STATE_KEY_LAST_REFRESH, 0); autoRefresh = savedInstanceState.getBoolean(STATE_KEY_AUTOREFRESH, false); if(savedInstanceState.containsKey(STATE_KEY_EXPANDED_ITEMS)) { expandedServices.clear(); Collections.addAll(expandedServices, savedInstanceState.getStringArray( STATE_KEY_EXPANDED_ITEMS)); } } else { autoRefresh = sp.getBoolean(PreferencesActivity.PREF_AUTO_REFRESH, false); } } /** * {@inheritDoc} */ @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View v = inflater.inflate(R.layout.displaystopdata, container, false); // Get the UI components we need. listView = (ExpandableListView)v.findViewById(android.R.id.list); txtLastRefreshed = (TextView)v.findViewById(R.id.txtLastUpdated); layoutTopBar = v.findViewById(R.id.layoutTopBar); txtStopName = (TextView)v.findViewById(R.id.txtStopName); txtServices = (TextView)v.findViewById(R.id.txtServices); txtError = (TextView)v.findViewById(R.id.txtError); progressSmall = (ProgressBar)v.findViewById(R.id.progressSmall); progressBig = (ProgressBar)v.findViewById(R.id.progressBig); imgbtnFavourite = (ImageButton)v.findViewById(R.id.imgbtnFavourite); imgbtnFavourite.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { // Add/remove as favourite. if(sd.getFavouriteStopExists(stopCode)) { callbacks.onShowConfirmFavouriteDeletion(stopCode); } else { callbacks.onShowAddFavouriteStop(stopCode, stopLocality != null ? stopName + ", " + stopLocality : stopName); } } }); // The ListView has a context menu. registerForContextMenu(listView); return v; } /** * {@inheritDoc} */ @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Tell the fragment that there is an options menu. setHasOptionsMenu(true); if(stopCode != null && stopCode.length() != 0) { setStopName(); // Since there is a stop code, there is no reason the bus service // list cannot be populated. txtServices.setText(BusStopDatabase.getColouredServiceListString( bsd.getBusServicesForStopAsString(stopCode))); if(getArguments().getBoolean(ARG_FORCELOAD, false)) { loadBusTimes(true); } else { loadBusTimes(false); } } else { handleError(BusParser.ERROR_NOCODE); } getArguments().remove(ARG_FORCELOAD); } /** * {@inheritDoc} */ @Override public void onResume() { super.onResume(); // Make sure there are no EVENT_UPDATE_TIME messages in the queue. mHandler.removeMessages(EVENT_UPDATE_TIME); // Set it up again. updateLastRefreshed(); setUpLastUpdated(); if (autoRefresh && !busTimesLoading) { setUpAutoRefresh(); } // Refresh the menu. getActivity().supportInvalidateOptionsMenu(); // Set the favourite ImageButton. if(sd.getFavouriteStopExists(stopCode)) { imgbtnFavourite.setImageResource(R.drawable.ic_list_favourite); imgbtnFavourite.setContentDescription( getString(R.string.favourite_rem)); } else { imgbtnFavourite.setImageResource( R.drawable.ic_list_unfavourite_light); imgbtnFavourite.setContentDescription( getString(R.string.favourite_add)); } } /** * {@inheritDoc} */ @Override public void onPause() { super.onPause(); // Stop the background tasks when we're pasued. mHandler.removeMessages(EVENT_REFRESH); mHandler.removeMessages(EVENT_UPDATE_TIME); } /** * {@inheritDoc} */ @Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(STATE_KEY_AUTOREFRESH, autoRefresh); outState.putLong(STATE_KEY_LAST_REFRESH, lastRefresh); populateExpandedItemsList(); if(!expandedServices.isEmpty()) { final String[] items = new String[expandedServices.size()]; outState.putStringArray(STATE_KEY_EXPANDED_ITEMS, expandedServices.toArray(items)); } } /** * {@inheritDoc} */ @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { // Inflate the menu. inflater.inflate(R.menu.displaystopdata_option_menu, menu); } /** * {@inheritDoc} */ @Override public void onPrepareOptionsMenu(final Menu menu) { super.onPrepareOptionsMenu(menu); // Get the menu items. final MenuItem sortItem = menu.findItem( R.id.displaystopdata_option_menu_sort); final MenuItem autoRefreshItem = menu.findItem( R.id.displaystopdata_option_menu_autorefresh); final MenuItem proxItem = menu.findItem( R.id.displaystopdata_option_menu_prox); final MenuItem timeItem = menu.findItem( R.id.displaystopdata_option_menu_time); final MenuItem refreshItem = menu.findItem( R.id.displaystopdata_option_menu_refresh); // If progress is being shown, disable the refresh button. if(progressBig.getVisibility() == View.VISIBLE || progressSmall.getVisibility() == View.VISIBLE) { refreshItem.setEnabled(false); } else { refreshItem.setEnabled(true); } // If there's no bus times, disable all other menu items. if(listView.getVisibility() == View.VISIBLE) { sortItem.setEnabled(true); proxItem.setEnabled(true); timeItem.setEnabled(true); } else { sortItem.setEnabled(false); proxItem.setEnabled(false); timeItem.setEnabled(false); } // Sort by time or service? if(sp.getBoolean(PreferencesActivity.PREF_SERVICE_SORTING, false)) { sortItem.setTitle(R.string.displaystopdata_menu_sort_service); } else { sortItem.setTitle(R.string.displaystopdata_menu_sort_times); } // Auto-refresh on or off? if(autoRefresh) { autoRefreshItem.setTitle( R.string.displaystopdata_menu_turnautorefreshoff); } else { autoRefreshItem.setTitle( R.string.displaystopdata_menu_turnautorefreshon); } // Proximity alert active or not? if(sd.isActiveProximityAlert(stopCode)) { proxItem.setTitle(R.string.displaystopdata_menu_prox_rem) .setIcon(R.drawable.ic_menu_proximityremove); } else { proxItem.setTitle(R.string.displaystopdata_menu_prox_add) .setIcon(R.drawable.ic_menu_proximityadd); } // Time alert active or not? if(sd.isActiveTimeAlert(stopCode)) { timeItem.setTitle(R.string.displaystopdata_menu_time_rem) .setIcon(R.drawable.ic_menu_arrivalremove); } else { timeItem.setTitle(R.string.displaystopdata_menu_time_add) .setIcon(R.drawable.ic_menu_arrivaladd); } } /** * {@inheritDoc} */ @Override public boolean onOptionsItemSelected(final MenuItem item) { switch(item.getItemId()) { case R.id.displaystopdata_option_menu_sort: // Change the sort preference and ask for a data redisplay. boolean sortByTime = sp.getBoolean( PreferencesActivity.PREF_SERVICE_SORTING, false); sortByTime = !sortByTime; final SharedPreferences.Editor edit = sp.edit(); edit.putBoolean(PreferencesActivity.PREF_SERVICE_SORTING, sortByTime); edit.commit(); loadBusTimes(false); getActivity().supportInvalidateOptionsMenu(); return true; case R.id.displaystopdata_option_menu_autorefresh: // Turn auto-refresh on or off. if(autoRefresh) { autoRefresh = false; mHandler.removeMessages(EVENT_REFRESH); } else { autoRefresh = true; setUpAutoRefresh(); } getActivity().supportInvalidateOptionsMenu(); return true; case R.id.displaystopdata_option_menu_refresh: // Ask for a refresh. mHandler.removeMessages(EVENT_REFRESH); loadBusTimes(true); return true; case R.id.displaystopdata_option_menu_prox: if(sd.isActiveProximityAlert(stopCode)) { callbacks.onShowConfirmDeleteProximityAlert(); } else { // Show the Activity for adding a new proximity alert. callbacks.onShowAddProximityAlert(stopCode); } return true; case R.id.displaystopdata_option_menu_time: if(sd.isActiveTimeAlert(stopCode)) { callbacks.onShowConfirmDeleteTimeAlert(); } else { // Show the Activity for adding a new time alert. callbacks.onShowAddTimeAlert(stopCode, null); } return true; default: return super.onOptionsItemSelected(item); } } /** * {@inheritDoc} */ @Override public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); // Create the ListView context menu. final MenuInflater inflater = getActivity().getMenuInflater(); menu.setHeaderTitle(getString(R.string.displaystopdata_context_title)); inflater.inflate(R.menu.displaystopdata_context_menu, menu); } /** * {@inheritDoc} */ @Override public boolean onContextItemSelected(final MenuItem item) { // Cast the information parameter. final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo)item.getMenuInfo(); switch(item.getItemId()) { case R.id.displaystopdata_context_menu_addarrivalalert: // Get the position where this data lives. final int position = ExpandableListView .getPackedPositionGroup(info.packedPosition); if(listAdapter != null && position < listAdapter.getGroupCount()) { final HashMap<String, String> groupData = (HashMap<String, String>)listAdapter .getGroup(position); // Fire off the Activity. callbacks.onShowAddTimeAlert(stopCode, new String[] { groupData.get(SERVICE_NAME_KEY) }); } return true; default: return super.onContextItemSelected(item); } } /** * {@inheritDoc} */ @Override public Loader<BusTimesResult> onCreateLoader(final int id, final Bundle args) { if(args == null) return null; showProgress(); busTimesLoading = true; return new BusTimesLoader(getActivity(), new EdinburghParser(), args.getStringArray(LOADER_ARG_STOPCODES), args.getInt(LOADER_ARG_NUMBER_OF_DEPARTURES, 4)); } /** * {@inheritDoc} */ @Override public void onLoadFinished(final Loader<BusTimesResult> loader, final BusTimesResult result) { busTimesLoading = false; if(result != null && isAdded()) { lastRefresh = result.getLastRefresh(); if(result.hasError()) { handleError(result.getError()); } else { displayData(result.getResult()); } } } /** * {@inheritDoc} */ @Override public void onLoaderReset(final Loader<BusTimesResult> loader) { // Nothing to do here. } private Handler mHandler = new Handler() { @Override public void handleMessage(final Message msg) { if (!isAdded()) { return; } switch(msg.what) { case EVENT_REFRESH: // Do a refresh. loadBusTimes(true); break; case EVENT_UPDATE_TIME: // Update the last update time. updateLastRefreshed(); setUpLastUpdated(); break; default: break; } } }; /** * Request new bus times. */ private void loadBusTimes(final boolean reload) { mHandler.removeMessages(EVENT_REFRESH); final Bundle args = new Bundle(); args.putStringArray(LOADER_ARG_STOPCODES, new String[] { stopCode }); args.putInt(LOADER_ARG_NUMBER_OF_DEPARTURES, numDepartures); if(reload) { getLoaderManager().restartLoader(0, args, this); } else { getLoaderManager().initLoader(0, args, this); } } /** * Handle errors. * * @param errorCode A number attributed to the error. */ private void handleError(final int errorCode) { switch(errorCode) { case BusParser.ERROR_NOCONNECTION: txtError.setText(R.string.displaystopdata_err_noconn); break; case BusParser.ERROR_CANNOTRESOLVE: txtError.setText(R.string.displaystopdata_err_noresolv); break; case BusParser.ERROR_NOCODE: txtError.setText(R.string.displaystopdata_err_nocode); break; case BusParser.ERROR_PARSEERR: txtError.setText(R.string.displaystopdata_err_parseerr); break; case BusParser.ERROR_NODATA: txtError.setText(R.string.displaystopdata_err_nodata); break; case BusParser.ERROR_URLMISMATCH: txtError.setText(R.string.displaystopdata_err_urlmismatch); break; case EdinburghParser.ERROR_INVALID_APP_KEY: txtError.setText(R.string .displaystopdata_err_api_invalid_key); break; case EdinburghParser.ERROR_INVALID_PARAMETER: txtError.setText(R.string .displaystopdata_err_api_invalid_parameter); break; case EdinburghParser.ERROR_PROCESSING_ERROR: txtError.setText(R.string .displaystopdata_err_api_processing_error); break; case EdinburghParser.ERROR_SYSTEM_MAINTENANCE: txtError.setText(R.string .displaystopdata_err_api_system_maintenance); break; case EdinburghParser.ERROR_SYSTEM_OVERLOADED: txtError.setText(R.string .displaystopdata_err_api_system_overloaded); break; default: txtError.setText(R.string.displaystopdata_err_unknown); break; } showError(); if(autoRefresh) { setUpAutoRefresh(); } } /** * Show progress indicators. If the ListView is not shown, then replace the * huge white space with a progress indicator. If the ListView is shown, * replace the last updated text with new text and a small progress * indicator. */ private void showProgress() { txtError.setVisibility(View.GONE); if(listView.getVisibility() == View.GONE) { layoutTopBar.setVisibility(View.GONE); progressBig.setVisibility(View.VISIBLE); } else { layoutTopBar.setVisibility(View.VISIBLE); progressBig.setVisibility(View.GONE); progressSmall.setVisibility(View.VISIBLE); } getActivity().supportInvalidateOptionsMenu(); } /** * Show the bus times. Ensure progress and error layouts are removed and * show the top bar and ListView. */ private void showTimes() { progressBig.setVisibility(View.GONE); txtError.setVisibility(View.GONE); progressSmall.setVisibility(View.INVISIBLE); layoutTopBar.setVisibility(View.VISIBLE); listView.setVisibility(View.VISIBLE); getActivity().supportInvalidateOptionsMenu(); layoutTopBar.post(new Runnable() { @Override public void run() { final Rect rect = new Rect(); imgbtnFavourite.getHitRect(rect); // Assume it's a square final int adjustBy = (int) ((hitboxSize - (rect.bottom - rect.top)) / 2); if(adjustBy > 0) { rect.top -= adjustBy; rect.bottom += adjustBy; rect.left -= adjustBy; rect.right += adjustBy; } layoutTopBar.setTouchDelegate(new TouchDelegate(rect, imgbtnFavourite)); } }); } /** * Show errors. Ensure progress and bus times layouts are removed and show * the error layout. */ private void showError() { layoutTopBar.setVisibility(View.GONE); listView.setVisibility(View.GONE); progressBig.setVisibility(View.GONE); progressSmall.setVisibility(View.GONE); txtError.setVisibility(View.VISIBLE); getActivity().supportInvalidateOptionsMenu(); } /** * Set the stop name. Firstly, it checks to see if there is a favourite stop * for this stop code and uses the user-set name. If not, it checks the bus * stop database and uses that name. Otherwise, it will use empty String * for it to be replaced later when the times are loaded with the name from * the bus tracker web service. */ private void setStopName() { if(sd.getFavouriteStopExists(stopCode)) { stopName = sd.getNameForStop(stopCode); } else { stopName = bsd.getNameForBusStop(stopCode); stopLocality = bsd.getLocalityForStopCode(stopCode); } if(stopName == null || stopName.length() == 0) { txtStopName.setText(stopCode); stopName = ""; } else { final String name; if(stopLocality != null) { name = getString(R.string.busstop_locality_coloured, stopName, stopLocality, stopCode); } else { name = getString(R.string.busstop_coloured, stopName, stopCode); } txtStopName.setText(Html.fromHtml(name)); } } /** * Display the data once loaded in the ListView. */ private void displayData(final HashMap<String, BusStop> data) { if(data == null) { // There must be no data. handleError(BusParser.ERROR_NODATA); return; } // Get the data for this stop code. final EdinburghBusStop busStop = (EdinburghBusStop)data.get(stopCode); if(busStop == null) { // There must be no data for this stop code. handleError(BusParser.ERROR_NODATA); return; } // If this is just a refresh, populate the expanded items list. if(listAdapter != null) { populateExpandedItemsList(); } // If the stopName could not be set earlier, get it now from the web // service. if(stopName == null || stopName.length() == 0) { stopName = busStop.getStopName(); final String name = getString(R.string.busstop_coloured, stopName, stopCode); // Show the user the stop name and stop code. txtStopName.setText(Html.fromHtml(name)); } // Get the list of services in the user's preferred order. final ArrayList<BusService> services; if(sp.getBoolean(PreferencesActivity.PREF_SERVICE_SORTING, false)) { services = busStop.getSortedByTimeBusServices(); } else { services = busStop.getBusServices(); } // Does the user want to show night services? final boolean showNightServices = sp.getBoolean(PreferencesActivity.PREF_SHOW_NIGHT_BUSES, true); // Declare variables before going in to the loop. final ArrayList<HashMap<String, String>> groupData = new ArrayList<HashMap<String, String>>(); final ArrayList<ArrayList<HashMap<String, String>>> childData = new ArrayList<ArrayList<HashMap<String, String>>>(); HashMap<String, String> curGroupMap; ArrayList<HashMap<String, String>> children; HashMap<String, String> curChildMap; EdinburghBus bus; String timeToDisplay, destination; int mins; boolean first; // Loop through the list of services. for(BusService busService : services) { if(!showNightServices && busService.getServiceName().startsWith("N")) continue; curGroupMap = new HashMap<String, String>(); groupData.add(curGroupMap); // Add the service name. curGroupMap.put(SERVICE_NAME_KEY, busService.getServiceName()); children = new ArrayList<HashMap<String, String>>(); first = true; // Loop through the buses inside a service. for(Bus lBus : busService.getBuses()) { bus = (EdinburghBus)lBus; destination = bus.getDestination(); if(bus.isDiverted()) { // Special case if diverted. timeToDisplay = ""; // Destination may be null when it comes back from the web // service. Display diverted notice accordingly. if(destination != null) { destination += " (" + getString(R.string.displaystopdata_diverted) + ')'; } else { destination = getString(R.string .displaystopdata_diverted); } } else { // Get the number of minutes until arrival. mins = bus.getArrivalMinutes(); if(mins > 59) { // If more than 59 minutes, display the full time. timeToDisplay = bus.getArrivalTime(); } else if(mins < 2) { // If the bus is due in less than 2 mins, show as due. timeToDisplay = "DUE"; } else { // Otherwise, display the number of minutes until // arrival. timeToDisplay = String.valueOf(mins); } // If the time is estimated, prefix this to the time shown. if(bus.isEstimated()) { timeToDisplay = '*' + timeToDisplay; } // If the destination is null, make it the empty string to // prevent future problems. if(destination == null) { destination = ""; } } if(first) { // If this is the first bus for this service, put this entry // in the group map. curGroupMap.put(DESTINATION_KEY, destination); curGroupMap.put(ARRIVAL_TIME_KEY, timeToDisplay); first = false; } else { // Otherwise, put it in the expanded child map. curChildMap = new HashMap<String, String>(); children.add(curChildMap); curChildMap.put(DESTINATION_KEY, destination); curChildMap.put(ARRIVAL_TIME_KEY, timeToDisplay); } } childData.add(children); } // Create the adatper. This is ugly. listAdapter = new BusTimesExpandableListAdapter( getActivity(), groupData, R.layout.expandable_list_group, new String[] { SERVICE_NAME_KEY, DESTINATION_KEY, ARRIVAL_TIME_KEY }, new int[] { R.id.buslist_service, R.id.buslist_destination, R.id.buslist_time }, childData, R.layout.expandable_list_child, new String[] { DESTINATION_KEY, ARRIVAL_TIME_KEY }, new int[] { R.id.buschild_destination, R.id.buschild_time }); listView.setAdapter(listAdapter); final int count = groupData.size(); for(int i = 0; i < count; i++) { curGroupMap = groupData.get(i); // Re-expand previously expanded items. if(expandedServices.contains(curGroupMap.get(SERVICE_NAME_KEY))) { listView.expandGroup(i); } } showTimes(); if(autoRefresh) setUpAutoRefresh(); updateLastRefreshed(); } /** * Update the text that informs the user how long it has been since the bus * data was last refreshed. This normally gets called about every 10 * seconds. */ private void updateLastRefreshed() { final long timeSinceRefresh = SystemClock.elapsedRealtime() - lastRefresh; final int mins = (int)(timeSinceRefresh / 60000); final String text; if(lastRefresh <= 0) { // The data has never been refreshed. text = getString(R.string.times_never); } else if(mins > 59) { // The data was refreshed more than 1 hour ago. text = getString(R.string.times_greaterthanhour); } else if(mins == 0) { // The data was refreshed less than 1 minute ago. text = getString(R.string.times_lessthanoneminago); } else { text = getResources() .getQuantityString(R.plurals.times_minsago, mins, mins); } txtLastRefreshed.setText(getString(R.string.displaystopdata_lastupdated, text)); } /** * Schedule the auto-refresh to execute again 60 seconds after the data was * last refreshed. */ private void setUpAutoRefresh() { mHandler.removeMessages(EVENT_REFRESH); final long time = (lastRefresh + AUTO_REFRESH_PERIOD) - SystemClock.elapsedRealtime(); if(time > 0) { mHandler.sendEmptyMessageDelayed(EVENT_REFRESH, time); } else { mHandler.sendEmptyMessage(EVENT_REFRESH); } } /** * Schedule the text which denotes the last update time to update in 10 * seconds. */ private void setUpLastUpdated() { mHandler.sendEmptyMessageDelayed(EVENT_UPDATE_TIME, LAST_REFRESH_PERIOD); } /** * This method populates the ArrayList of expanded list items. It will clear * the list and loop through the group items in the expanded items to see * if that item is expanded or not. If the item is expanded, the service * name will be added to the list. */ private void populateExpandedItemsList() { // Firstly, flush the previous items from the list. expandedServices.clear(); // The ListAdapter could be null. if(listAdapter != null) { // Cache the count. final int count = listAdapter.getGroupCount(); HashMap<String, String> groupData; // Loop through all group items. for(int i = 0; i < count; i++) { // If the group is expanded, get the service name and add it to // the list. if(listView.isGroupExpanded(i)) { groupData = (HashMap<String, String>)listAdapter .getGroup(i); expandedServices.add(groupData.get(SERVICE_NAME_KEY)); } } } } /** * {@inheritDoc} */ @Override public void onConfirmFavouriteDeletion() { imgbtnFavourite.setImageResource( R.drawable.ic_list_unfavourite_light); imgbtnFavourite.setContentDescription( getString(R.string.favourite_add)); } /** * {@inheritDoc} */ @Override public void onCancelFavouriteDeletion() { // Nothing to do here. } /** * This custom ExpandableListAdapter attributes colours to service names * in the ExpandableListView. */ private static class BusTimesExpandableListAdapter extends SimpleExpandableListAdapter { private final Context context; private final int defaultColour; private final HashMap<String, String> colours; /** * Create a new BusTimesExpandableListAdapter. * * @param context A Context instance. * @param groupData The group data. * @param groupLayout The layout to use for the group View. * @param groupFrom An array of keys to use for the group items. * @param groupTo The TextViews to load the keys in to. * @param childData The child data. * @param childLayout The layout to use for the child View. * @param childFrom An array of keys to use for the child View. * @param childTo The TextViews to load the keys in to. */ public BusTimesExpandableListAdapter(final Context context, final ArrayList<HashMap<String, String>> groupData, final int groupLayout, final String[] groupFrom, final int[] groupTo, final ArrayList<ArrayList<HashMap<String, String>>> childData, final int childLayout, final String[] childFrom, final int[] childTo) { super(context, groupData, groupLayout, groupFrom, groupTo, childData, childLayout, childFrom, childTo); // The superclass has no way to get the context again, so cache it // here. this.context = context; final BusStopDatabase bsd = BusStopDatabase.getInstance( context.getApplicationContext()); defaultColour = context.getResources().getColor(R.color .defaultBusColour); final int size = groupData.size(); // Create an array of String to hold the loaded services. final String[] services = new String[size]; int i = 0; // Get the service list from the group data and put it in the // service array. for(HashMap<String, String> map : groupData) { services[i] = map.get(SERVICE_NAME_KEY); i++; } if(size > 0) { colours = bsd.getServiceColours(services); } else { colours = null; } } /** * {@inheritDoc} */ @Override @SuppressLint({"NewAPI"}) public View getGroupView(final int groupPosition, final boolean isExpanded, final View convertView, final ViewGroup parent) { final View v = super.getGroupView(groupPosition, isExpanded, convertView, parent); final TextView txtService = (TextView)v.findViewById( R.id.buslist_service); // Get the HashMap for the groupPosition. final HashMap<String, String> group = (HashMap<String, String>)getGroup(groupPosition); // Get the name of the service. final String service = group.get(SERVICE_NAME_KEY); // Get the Drawable which makes up the retangle in the background // with the rounded corners. Make it mutable so it doesn't affect // other instances of the same Drawable. final GradientDrawable background; try { background = (GradientDrawable)context.getResources() .getDrawable(R.drawable.bus_service_rounded_background) .mutate(); } catch(ClassCastException e) { txtService.setTextColor(Color.BLACK); return v; } // Night services are treated differently to the rest. if(service.startsWith("N")) { // Give it a black background. background.setColor(Color.BLACK); // We need to replace the text in the TextView because HTML // formatting has been applied to it, to make the 'N' red. txtService.setText( BusStopDatabase.getColouredServiceListString(service)); } else if(colours != null && colours.containsKey(service)) { try { // If the colour for the service can be parsed, set the // background here. background.setColor(Color.parseColor( colours.get(service))); } catch(IllegalArgumentException e) { // If it cannot be parsed, use the default background // colour. background.setColor(defaultColour); } } else { // If not a night service, and a colour doesn't exist for the // service, use the default colour. background.setColor(defaultColour); } // Set the background and return the View for the group. if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { txtService.setBackground(background); } else { txtService.setBackgroundDrawable(background); } return v; } } /** * Any Activities which host this Fragment must implement this interface to * handle navigation events. */ public static interface Callbacks { /** * This is called when it should be confirmed with the user that they * want to delete a favourite bus stop. * * @param stopCode The bus stop that the user may want to delete. */ public void onShowConfirmFavouriteDeletion(String stopCode); /** * This is called when it should be confirmed with the user that they * want to delete the proximity alert. */ public void onShowConfirmDeleteProximityAlert(); /** * This is called when it should be confirmed with the user that they * want to delete the time alert. */ public void onShowConfirmDeleteTimeAlert(); /** * This is called when the user wants to add a new favourite bus stop. * * @param stopCode The stop code of the bus stop to add. * @param stopName The default name to use for the bus stop. */ public void onShowAddFavouriteStop(String stopCode, String stopName); /** * This is called when the user wants to view the interface to add a new * proximity alert. * * @param stopCode The stopCode the proximity alert should be added for. */ public void onShowAddProximityAlert(String stopCode); /** * This is called when the user wants to view the interface to add a new * time alert. * * @param stopCode The stopCode the time alert should be added for. * @param defaultServices The services that should be selected by * default. Set to null if no services should be selected. */ public void onShowAddTimeAlert(String stopCode, String[] defaultServices); } }